//@author A0081058H
package net.mysoc.w111j.ui.main;

import java.util.regex.Pattern;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import net.mysoc.w111j.model.Money;
import net.mysoc.w111j.ui.validation.Validation;
import net.mysoc.w111j.ui.validation.ValidationMessage;
import net.mysoc.w111j.ui.validation.ValidationState;
import net.mysoc.w111j.util.Layout;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The view for the main application. Layouts the main UI, controls, and contains the property for scene changing.
 * Change the scene by changing the {@code MainModel}'s contentProperty -- call setContent(Node content).
 */
public class MainView extends BorderPane {
    private static Logger logger = LoggerFactory.getLogger(MainPresenter.class);

    private final MainModel model;
    private MainPresenter presenter;
    private BorderPane contentArea;
    private final BorderPane thisPane;
    // The following are variables here so we can requestFocus() them.
    // This is required for keyboard shortcuts.
    private TextField commandLineField;
    private TextField searchField;
    private TextField budgetField;

    /**
     * Builds the view and links the {@code MainModel}'s content to the view.
     *
     * @param model the {@code MainModel}. What is shown in the main panel is in the MainModel's content property.
     */
    public MainView(MainModel model) {
        this.model = model;
        thisPane = this;
        buildView();
    }

    /**
     * Sets the {@code MainPresenter} linked to this view.
     *
     * @param presenter the {@code MainPresenter} linked to this view.
     */
    public void setPresenter(MainPresenter presenter) {
        this.presenter = presenter;
    }

    /**
     * Builds the view.
     */
    private void buildView() {
        setTop(addTopBar());
        this.contentArea = new BorderPane();
        this.contentArea.centerProperty().bind(model.contentProperty());
        setCenter(this.contentArea);
        setBottom(addBottomBar());
    }

    /**
     * Creates and returns the {@code HBox} representing the top bar of the UI. Contains the budget, add expense button
     * and search box.
     *
     * @return the top bar of the UI.
     */
    private Node addTopBar() {
        HBox topBar = new HBox();
        topBar.getStyleClass().add("main-topbar");
        topBar.getChildren().addAll(addAddExpenseButton(), Layout.hboxFloatRight(), addBudget(),
                addGenerateReportButton(), addSearchBox());

        return topBar;
    }

    /**
     * Creates and returns the budget control box.
     * FIXME: Properly use a Money object instead of just using a String. See ExpenseList on how to do this right
     *        (how it keeps track of category names)
     *
     * @return a {@code HBox} containing the current expenditure and budget {@code TextField}.
     */
    private HBox addBudget() {
        HBox hbox = new HBox();
        final Text prepend = new Text("$");
        final Label expenditure = new Label();
        Text separator = new Text(" / $");
        this.budgetField = new TextField();

        this.budgetField.setPromptText("Budget");
        expenditure.setTooltip(new Tooltip("Current month's expenditure"));
        this.budgetField.setTooltip(new Tooltip("Monthly budget (Ctrl+B)"));

        expenditure.textProperty().bindBidirectional(this.model.expenditureProperty());
        this.budgetField.textProperty().bindBidirectional(this.model.budgetProperty());

        // Change expenditure colour when overspending.
        ChangeListener<Object> alert = new ChangeListener<Object>() {
            @Override
            public void changed(ObservableValue<?> o, Object oldVal, Object newVal) {
                ValidationMessage budgetValidation = Validation.validateMoney(model.getBudget());
                ValidationMessage expenditureValidation = Validation.validateMoney(model.getExpenditure());

                if (budgetValidation.getState() != ValidationState.ERROR
                        && expenditureValidation.getState() != ValidationState.ERROR) {
                    if (new Money(model.getBudget()).compareTo(new Money(model.getExpenditure())) < 0) {
                        // Note: the style is hardcoded here.
                        // Apparently JavaFX caches CSS, so alert colour does not update instantly on CSS class
                        // addition/deletion

                        logger.debug("ALERT: Overspending");
                        expenditure.setTextFill(Color.RED);
                        prepend.setFill(Color.RED);
                        // expenditure.getStyleClass().add("main-view-budgetbox-alert");
                        // expenditure.getStyleClass().add("main-view-budgetbox-noalert");
                    } else {
                        expenditure.setTextFill(Color.WHITE);
                        prepend.setFill(Color.WHITE);
                        // expenditure.getStyleClass().add("main-view-budgetbox-noalert");
                        // expenditure.getStyleClass().remove("main-view-budgetbox-alert");
                    }
                }
            }
        };

        // Reset invalid budget text field to user budget on focus loss.
        ChangeListener<Object> resetBudget = new ChangeListener<Object>() {
            @Override
            public void changed(ObservableValue<?> o, Object oldVal, Object newVal) {
                ValidationMessage budgetValidation = Validation.validateMoney(model.getBudget());

                if (budgetValidation.getState() == ValidationState.ERROR) {
                    budgetField.setText(presenter.handleGetUserBudget().toPlainString());
                } else if (budgetValidation.getState() == ValidationState.OK) {
                    presenter.handleBudgetFieldInput();
                }
            }
        };

        this.model.expenditureProperty().addListener(alert);
        this.model.budgetProperty().addListener(alert);
        this.budgetField.focusedProperty().addListener(resetBudget);
        this.budgetField.setMinWidth(50);
        this.budgetField.setPrefWidth(80);

        // Consume characters other than digits and full stop.
        this.budgetField.addEventFilter(KeyEvent.KEY_TYPED, new EventHandler<KeyEvent>() {
            private final Pattern decimalPattern = Pattern.compile("[\\d\\.]");

            @Override
            public void handle(KeyEvent event) {
                if (!decimalPattern.matcher(event.getCharacter()).matches()) {
                    event.consume();
                }
            }
        });

        // Focuses the window so textfield does not still have focus on ENTER
        this.budgetField.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                presenter.handleBudgetFieldInput();
                if (budgetField.isFocused()) {
                    thisPane.requestFocus();
                }
            }
        });

        // prepend.getStyleClass().add("main-topbar-budgetbox-prepend");
        // expenditure.getStyleClass().add("main-topbar-budgetbox-expenditure");
        separator.getStyleClass().add("main-topbar-budgetbox-separator");
        budgetField.getStyleClass().add("main-topbar-budgetbox-budget");
        hbox.getStyleClass().add("main-topbar-budgetbox");
        hbox.getChildren().addAll(prepend, expenditure, separator, budgetField);

        return hbox;
    }

    /**
     * Creates, links to {@code MainPresenter}, and returns a search box.
     *
     * @return a {@code HBox} containing a {@code TextField} linked to the {@code ExpenseService}
     */
    private HBox addSearchBox() {
        HBox searchBox = new HBox();
        this.searchField = new TextField();
        this.searchField.setPromptText("Search");
        this.searchField.setTooltip(new Tooltip("Search (Ctrl+F)"));
        this.model.searchFieldProperty().bind(this.searchField.textProperty());

        // Instant search
        this.searchField.textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observable, String oldVal, String newVal) {
                System.out.print("Instant search: ");
                presenter.handleSearchFieldInput();
            }
        });

        searchBox.getStyleClass().add("main-topbar-searchbox");
        this.searchField.getStyleClass().addAll("main-topbar-searchbox-searchfield");
        searchBox.getChildren().addAll(searchField);

        return searchBox;
    }

    /**
     * Creates, links to {@code MainPresenter}, and returns a {@code HBox} containing the Add Expense {@code Button}.
     *
     * @return a {@code HBox} containing the "Add Expense" {@code Button}.
     */
    private HBox addAddExpenseButton() {
        HBox addAddExpenseButtonBox = new HBox();
        Button addExpenseButton = new Button("New expense", new ImageView("plusw16alt.png"));
        addExpenseButton.setTooltip(new Tooltip("New expense (Ctrl+N)"));

        addExpenseButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                presenter.handleAddExpense();
            }
        });

        addExpenseButton.getStyleClass().add("main-topbar-addexpensebuttonbox-addexpensebutton");
        addAddExpenseButtonBox.getStyleClass().add("main-topbar-addexpensebuttonbox");
        addAddExpenseButtonBox.getChildren().addAll(addExpenseButton);

        return addAddExpenseButtonBox;
    }

    /**
     * Creates and returns the {@code HBox} containing the {@code Button} for the geneerate reports window.
     *
     * @return the button for generate reports.
     */
    private HBox addGenerateReportButton() {
        HBox generateReportButtonBox = new HBox();
        Button generateReportButton = new Button("Report", new ImageView("chartw16.png"));
        generateReportButton.setTooltip(new Tooltip("Report (Ctrl+R)"));

        generateReportButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                presenter.handleGenerateReport();
            }
        });

        generateReportButton.getStyleClass().add("main-topbar-generatereportbuttonbox-generatereportbutton");
        generateReportButtonBox.getStyleClass().add("main-topbar-generatereportbuttonbox");
        generateReportButtonBox.getChildren().addAll(generateReportButton);

        return generateReportButtonBox;
    }

    /**
     * Creates and returns the {@code HBox} representing the bottom bar of the UI. Contains the command line and the
     * undo button.
     *
     * @return the bottom bar of the UI.
     */
    private HBox addBottomBar() {
        HBox bottomBar = new HBox();
        HBox commandLine = addCommandLine();
        HBox statusLine = new HBox();
        HBox undoButton = addUndoButton();
        undoButton.setMinWidth(65);

        bottomBar.getStyleClass().add("main-bottombar");
        statusLine.getStyleClass().add("main-bottombar-statusline");

        statusLine.getChildren().addAll(addStatusBar(), undoButton);
        bottomBar.getChildren().addAll(commandLine, Layout.hboxFloatRight(), statusLine);

        return bottomBar;
    }

    /**
     * Creates, links to {@code MainPresenter}, and returns a {@code HBox} containing the command line.
     *
     * @return a {@code HBox} containing a {@code TextField} linked to the ...?
     */
    private HBox addCommandLine() {
        HBox commandLineBox = new HBox();
        this.commandLineField = new TextField();
        this.commandLineField.setMinWidth(200);
        this.commandLineField.setPrefWidth(200);
        this.commandLineField.setPromptText("Enter command");
        this.commandLineField.setTooltip(new Tooltip("Jump to command line (Ctrl+;)"));
        this.commandLineField.textProperty().bindBidirectional(this.model.commandLineProperty());

        // Expand textfield
        this.commandLineField.textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observable, String oldVal, String newVal) {
                commandLineField.setPrefWidth(commandLineField.getText().length() * 7);
            }
        });

        // Clear on VK_ENTER
        this.commandLineField.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                presenter.handleCommandLineInput();
                commandLineField.clear();
            }
        });

        commandLineBox.getStyleClass().add("main-bottombar-commandlinebox");
        this.commandLineField.getStyleClass().add("main-bottombar-commandlinefield");
        commandLineBox.getChildren().addAll(commandLineField);

        return commandLineBox;
    }

    /**
     * Focuses the search field.
     */
    public void focusSearchField() {
        this.searchField.requestFocus();
    }

    /**
     * Focuses the command line.
     */
    public void focusCommandLine() {
        this.commandLineField.requestFocus();
    }

    /**
     * Focuses the budget textfield.
     */
    public void focusBudgetField() {
        this.budgetField.requestFocus();

    }

    /**
     * Creates, links to {@code MainPresenter}, and returns a {@code HBox} containing the command line.
     *
     * @return a {@code HBox} containing a {@code Button} linked to the {@code MainPresenter}.
     */
    private HBox addUndoButton() {
        HBox undoButtonBox = new HBox();
        Button undoButton = new Button("Undo", new ImageView("undow12.png"));
        undoButton.setTooltip(new Tooltip("Undo (Ctrl+Z)"));

        undoButton.disableProperty().bindBidirectional(this.model.undoButtonDisableProperty());

        undoButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                presenter.handleUndoButton();
            }
        });

        undoButton.getStyleClass().addAll("main-bottombar-undobuttonbox-undobutton", "typo");
        undoButtonBox.getStyleClass().add("main-bottombar-undobuttonbox");
        undoButtonBox.getChildren().addAll(undoButton);

        return undoButtonBox;
    }

    /**
     * Creates, links to {@code MainModel}, and returns a {@code HBox} containing the status bar.
     *
     * @return a {@code HBox} containing the status bar.
     */
    private HBox addStatusBar() {
        HBox hbox = new HBox();
        Label status = new Label();
        status.textProperty().bind(this.model.statusProperty());

        status.getStyleClass().addAll("typo", "body");
        hbox.getStyleClass().addAll("main-bottombar-statusbar");
        hbox.getChildren().add(status);

        return hbox;
    }
}
